/*
 * MIT License
 *
 * Copyright (c) 2025 milkpotatoes
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

export { };

const BASE64_ALPHABET_PAD = '=';
const BASE64_BYTE_MASK = 0x3f;

const BASE64_ONE_BYTE = 6;
const BASE64_TWO_BYTE = 12;
const BASE64_THREE_BYTE = 18;

const BASE64_ONE_CHAR = 8;
const BASE64_TWO_CHAR = 16;

const BASE64_OFFSET_ONE = 1;
const BASE64_OFFSET_TWO = 2;

const BASE64_ENCODE_STEP = 3;

const base64AlphabetArray = [
    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
    'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
    'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
    'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
    'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
    'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
    'w', 'x', 'y', 'z', '0', '1', '2', '3',
    '4', '5', '6', '7', '8', '9', '+', '/',
];

const base64AlphabetMap: Map<number, string> = new Map();
const base64CodeMap: Map<string, number> = new Map();

for (let i = 0; i < base64AlphabetArray.length; i++) {
    const c = base64AlphabetArray[i];
    base64AlphabetMap.set(i, c);
    base64CodeMap.set(c, i);
}

class InvalidCharacterError extends Error {
    constructor(msg: string) { super(msg); }
}

const getAndCheckCode = (str: string, idx: number): number => {
    if (idx > str.length) return 0;
    const code = str.codePointAt(idx)!;
    if (code > 0xff)
        throw new InvalidCharacterError("Failed to execute 'btoa' on 'global': " +
            "The string to be encoded contains characters outside of the Latin1 range.");
    return code;
}

function btoa(data: string): string {
    if (typeof data !== 'string') data = String(data);
    let result = '';
    for (let i = 0; i < data.length; i += BASE64_ENCODE_STEP) {
        const byte = (getAndCheckCode(data, i) << BASE64_TWO_CHAR) |
            (getAndCheckCode(data, i + BASE64_OFFSET_ONE) << BASE64_ONE_CHAR) |
            getAndCheckCode(data, i + BASE64_OFFSET_TWO);
        const encoded1 = base64AlphabetMap.get((byte >>> BASE64_THREE_BYTE) & BASE64_BYTE_MASK)!;
        const encoded2 = base64AlphabetMap.get((byte >>> BASE64_TWO_BYTE) & BASE64_BYTE_MASK);
        const encoded3 = base64AlphabetMap.get((byte >>> BASE64_ONE_BYTE) & BASE64_BYTE_MASK);
        const encoded4 = base64AlphabetMap.get(byte & BASE64_BYTE_MASK);
        result += encoded1 + encoded2 +
            (i + BASE64_OFFSET_ONE < data.length ? encoded3 : BASE64_ALPHABET_PAD) +
            (i + BASE64_OFFSET_TWO < data.length ? encoded4 : BASE64_ALPHABET_PAD);
    }
    return result;
}

function atob(data: string): string {
    if (typeof data !== 'string') data = String(data);
    data = data.trim();
    let result = '';
    let bLen = 0;
    let code = 0;
    let end = 0;
    for (let i = 0; i < data.length; i++) {
        code <<= BASE64_ONE_BYTE;
        const c = data[i];
        switch (c) {
            case ' ':
            case '\xa0':
            case '\x85':
            case '\r':
            case '\n':
            case '\t':
            case '\f':
            case '\v':
                continue;
        }
        const b = base64CodeMap.get(c);
        if (b === undefined) {
            if (c !== BASE64_ALPHABET_PAD) {
                throw new InvalidCharacterError("Failed to execute 'atob' on 'global': " +
                    "The string to be decoded is not correctly encoded.");
            } else {
                end++;
                if (end > 2) {
                    throw new InvalidCharacterError("Failed to execute 'atob' on 'global': " +
                        "The string to be decoded is not correctly encoded.");
                }
                continue;
            }
        } else if (end > 0) {
            throw new InvalidCharacterError("Failed to execute 'atob' on 'global': " +
                "The string to be decoded is not correctly encoded.");
        }
        code += b;
        bLen += BASE64_ONE_BYTE;
        if (bLen >= BASE64_ONE_CHAR) {
            bLen -= BASE64_ONE_CHAR;
            const c = code >> bLen;
            code -= c << bLen;
            result += String.fromCodePoint(c);
        }
    }
    return result;
}

Object.defineProperties(globalThis, {
    'atob': {
        value: atob,
        configurable: true,
        writable: true,
        enumerable: false
    },
    'btoa': {
        value: btoa,
        configurable: true,
        writable: true,
        enumerable: false
    },
    'InvalidCharacterError': {
        value: InvalidCharacterError,
        configurable: true,
        writable: true,
        enumerable: false
    },
});
